<html>
 <head>
  <meta charset="UTF-8">
 </head>
 <body>
  <h1 data-lake-id="nckJM" id="nckJM"><span data-lake-id="ue8829d5d" id="ue8829d5d">典型回答</span></h1>
  <p data-lake-id="uefd3994a" id="uefd3994a"><br></p>
  <p data-lake-id="u690a4dde" id="u690a4dde"><span data-lake-id="u51d1976c" id="u51d1976c">这是一个典型的</span><strong><span data-lake-id="uf27f25a9" id="uf27f25a9">数据库热点数据更新</span></strong><span data-lake-id="u91c1f209" id="u91c1f209">的问题。</span></p>
  <p data-lake-id="u43cf2ab2" id="u43cf2ab2"><span data-lake-id="u221a24bd" id="u221a24bd">​</span><br></p>
  <p data-lake-id="ue008898a" id="ue008898a"><span data-lake-id="u775c8e9a" id="u775c8e9a">我们都知道，当多个并发事务同时尝试更新同一行热点数据时，可能会导致锁竞争和冲突。这会增加事务的等待时间和冲突概率，导致性能下降，并可能引发死锁问题。</span></p>
  <p data-lake-id="u36a2c36e" id="u36a2c36e"><span data-lake-id="u7d781870" id="u7d781870">​</span><br></p>
  <p data-lake-id="u3a84e71d" id="u3a84e71d"><span data-lake-id="u4a4e4197" id="u4a4e4197">详见以下两个问题，都是热点数据并发更新导致的：</span></p>
  <p data-lake-id="u0f1e9e27" id="u0f1e9e27"><span data-lake-id="udb1a1146" id="udb1a1146">​</span><br></p>
  <p data-lake-id="u884d7c3e" id="u884d7c3e"><br></p>
  <p data-lake-id="ub08a0b75" id="ub08a0b75"><br></p>
  <p data-lake-id="ub49734b6" id="ub49734b6"><br></p>
  <p data-lake-id="u1cd86320" id="u1cd86320"><span data-lake-id="ucfaefbc7" id="ucfaefbc7">这类问题的解决思路可以分为3类，分别是排队、拆分以及批次执行。</span></p>
  <p data-lake-id="u37c0ce5d" id="u37c0ce5d"><span data-lake-id="ua64a3dcb" id="ua64a3dcb">​</span><br></p>
  <h3 data-lake-id="isUEr" id="isUEr"><span data-lake-id="uccebba43" id="uccebba43">排队</span></h3>
  <p data-lake-id="u7227fee1" id="u7227fee1"><span data-lake-id="uc84d7abc" id="uc84d7abc">首先，我们常见的排队方案就是要么加锁、要么单线程执行。</span></p>
  <p data-lake-id="u458eff46" id="u458eff46"><span data-lake-id="ud0b74408" id="ud0b74408">​</span><br></p>
  <p data-lake-id="u4408d232" id="u4408d232"><span data-lake-id="u52aeaeef" id="u52aeaeef">随意，如果能使用Redis的话，那么就可以利用他的高并发、单线程特点来解决这个问题，因为题目要求是只能使用数据库，所以这个就不展开讲了，这种方案，在秒杀场景中用的还挺多的，可以参考：</span></p>
  <p data-lake-id="u1119e62c" id="u1119e62c"><span data-lake-id="ub3de775f" id="ub3de775f">​</span><br></p>
  <p data-lake-id="ucd5942fc" id="ucd5942fc"><span data-lake-id="u1f3041f6" id="u1f3041f6">​</span><br></p>
  <p data-lake-id="u508290cc" id="u508290cc"><span data-lake-id="ua9c14cb8" id="ua9c14cb8">除了上面这种基于Redis排队的方案，其实基于数据库也是可以的。</span></p>
  <p data-lake-id="u7740f9bf" id="u7740f9bf"><span data-lake-id="u7fbab5b0" id="u7fbab5b0">​</span><br></p>
  <blockquote data-lake-id="uf4733d37" id="uf4733d37">
   <p data-lake-id="u2be1ae23" id="u2be1ae23"><span data-lake-id="u6be425f7" id="u6be425f7">正常的update竞争抢锁的时候，也是排队的，但是这个会让事务持续自旋抢锁，严重耗费数据库CPU</span></p>
  </blockquote>
  <p data-lake-id="u8b5ca164" id="u8b5ca164"><br></p>
  <p data-lake-id="u23131e46" id="u23131e46"><strong><span data-lake-id="u4507daf1" id="u4507daf1">给大家提供一个思路，那就是在数据库层面做改造。比如腾讯云Mysql和阿里云Mysql都做过类似的优化。</span></strong></p>
  <p data-lake-id="u00f8ba23" id="u00f8ba23"><span data-lake-id="ua5e0f787" id="ua5e0f787">​</span><br></p>
  <p data-lake-id="ueb18dcff" id="ueb18dcff"><span data-lake-id="uf47d4ac4" id="uf47d4ac4">腾讯云数据库MySQL热点更新： </span><a href="https://cloud.tencent.com/document/product/236/63239" target="_blank" data-lake-id="ub4e9fa20" id="ub4e9fa20"><span data-lake-id="u77d8b121" id="u77d8b121">https://cloud.tencent.com/document/product/236/63239</span></a></p>
  <p data-lake-id="u32a80f92" id="u32a80f92"><span data-lake-id="u083073c9" id="u083073c9">阿里云数据库Inventory Hint： </span><a href="https://www.alibabacloud.com/help/zh/apsaradb-for-rds/latest/inventory-hint" target="_blank" data-lake-id="u0e4ec6cf" id="u0e4ec6cf"><span data-lake-id="u61ac987a" id="u61ac987a">https://www.alibabacloud.com/help/zh/apsaradb-for-rds/latest/inventory-hint</span></a></p>
  <p data-lake-id="u6a26f6a0" id="u6a26f6a0"><span data-lake-id="ua4b47d06" id="ua4b47d06">​</span><br></p>
  <p data-lake-id="u52309723" id="u52309723"><span data-lake-id="u5efa2892" id="u5efa2892">主要思路就是，针对于频繁更新或秒杀类业务场景，大幅度优化对于热点行数据的update操作的性能。当开启热点更新自动探测时，系统会自动探测是否有单行的热点更新，如果有，则会让大量的并发 update 排队执行，以减少大量行锁造成的并发性能下降。</span></p>
  <p data-lake-id="ufc4007ce" id="ufc4007ce"><span data-lake-id="u4dce6df8" id="u4dce6df8">​</span><br></p>
  <p data-lake-id="ufb1f69fd" id="ufb1f69fd"><span data-lake-id="uea699a78" id="uea699a78">也就是说，他们改造了MySQL数据库，让同一个热点行的更新语句，在执行层进行排队。这样的排队相比update的排队，要轻量级很多，因为他不需要自旋，不需要抢锁。</span></p>
  <p data-lake-id="uce27f964" id="uce27f964"><span data-lake-id="ue98d18b5" id="ue98d18b5" class="lake-fontsize-12" style="color: rgb(85, 85, 85)">​</span><br></p>
  <blockquote data-lake-id="u889224ab" id="u889224ab">
   <p data-lake-id="ufab16bb6" id="ufab16bb6"><span data-lake-id="u9e3d553d" id="u9e3d553d" class="lake-fontsize-12" style="color: rgb(85, 85, 85)">阿里的数据</span><span data-lake-id="uce29fd83" id="uce29fd83">库还提供了事务性Hint来控制并发和快速提交/回滚事务，提高业务吞吐能力，上面的链接中也有介绍，可以自行了解下。</span></p>
  </blockquote>
  <p data-lake-id="u35ba0a3f" id="u35ba0a3f"><span data-lake-id="u7cc9dda3" id="u7cc9dda3" class="lake-fontsize-12" style="color: rgb(85, 85, 85)">​</span><br></p>
  <p data-lake-id="u12769457" id="u12769457"><span data-lake-id="uaf0b5aa1" id="uaf0b5aa1" class="lake-fontsize-12" style="color: rgb(85, 85, 85)">具体原理参见：</span></p>
  <p data-lake-id="uaea323f0" id="uaea323f0"><span data-lake-id="uc686ceba" id="uc686ceba" class="lake-fontsize-12" style="color: rgb(85, 85, 85)">​</span><br></p>
  <p data-lake-id="ue29b7135" id="ue29b7135"><span data-lake-id="u3553b0ad" id="u3553b0ad" class="lake-fontsize-12" style="color: rgb(85, 85, 85)">​</span><br></p>
  <h3 data-lake-id="C6foZ" id="C6foZ"><span data-lake-id="u46879494" id="u46879494">拆分</span></h3>
  <p data-lake-id="ueff0b77f" id="ueff0b77f"><br></p>
  <p data-lake-id="u689c7251" id="u689c7251"><span data-lake-id="u11e9ac27" id="u11e9ac27" class="lake-fontsize-12">除了排队，还可以做一些拆分操作，比如说把一个大的库存拆分成多个小库存，有点类似于concurrentHashMap中的分段锁的机制，通过拆分的手段降低锁的粒度来提升并发度。</span></p>
  <p data-lake-id="u664c9af2" id="u664c9af2"><span data-lake-id="u54c6edbb" id="u54c6edbb" class="lake-fontsize-12">​</span><br></p>
  <p data-lake-id="u2bdbeb62" id="u2bdbeb62"><span data-lake-id="u4540b252" id="u4540b252" class="lake-fontsize-12">拆分后，一次扣减动作就可以分散到不同的库、表中进行。</span></p>
  <p data-lake-id="u0890bbe4" id="u0890bbe4"><span data-lake-id="u620187e3" id="u620187e3" class="lake-fontsize-12">​</span><br></p>
  <p data-lake-id="ub36383b8" id="ub36383b8"><span data-lake-id="u8fcb32c2" id="u8fcb32c2" class="lake-fontsize-12">但是这个方案存在一个比较大的问题，那就是可能会存在碎片。</span></p>
  <p data-lake-id="u3b522cdc" id="u3b522cdc"><span data-lake-id="u77ffd230" id="u77ffd230" class="lake-fontsize-12">​</span><br></p>
  <p data-lake-id="u19a434d6" id="u19a434d6"><span data-lake-id="uea6d81f2" id="uea6d81f2" class="lake-fontsize-12">假如一共有1000个库存，在一起的话，是可以扣减999个的。但是如果分散到多个子库存中，那么就会没办法一次性扣减999。</span></p>
  <p data-lake-id="uedef814d" id="uedef814d"><span data-lake-id="u5ee0638a" id="u5ee0638a" class="lake-fontsize-12">​</span><br></p>
  <p data-lake-id="u441d016e" id="u441d016e"><span data-lake-id="ufc4e318a" id="ufc4e318a" class="lake-fontsize-12">当然，也是可以通过一些手段来解决的，比如最库存的腾挪或者超占等，但是整体看上去方案并不完美。所以这个方案用得不多。</span></p>
  <p data-lake-id="u0042a92c" id="u0042a92c"><span data-lake-id="u51ea4172" id="u51ea4172" class="lake-fontsize-12">​</span><br></p>
  <p data-lake-id="u39efa049" id="u39efa049"><span data-lake-id="u398beced" id="u398beced" class="lake-fontsize-12">​</span><br></p>
  <h3 data-lake-id="wANIh" id="wANIh"><span data-lake-id="u32f5cac8" id="u32f5cac8">合并</span></h3>
  <p data-lake-id="ud1ec5e87" id="ud1ec5e87"><br></p>
  <p data-lake-id="ud76ad043" id="ud76ad043"><span data-lake-id="u1b200776" id="u1b200776" class="lake-fontsize-12">除了拆分以外，还有一种合并批量执行的方式。</span></p>
  <p data-lake-id="uffb4b4a1" id="uffb4b4a1"><span data-lake-id="uefc76597" id="uefc76597" class="lake-fontsize-12">​</span><br></p>
  <p data-lake-id="u0d9eb87b" id="u0d9eb87b"><span data-lake-id="u62e05ed5" id="u62e05ed5" class="lake-fontsize-12">多条update在一起会有锁争抢，那如果把多个UPDATE合成一个UPDATE不就可以降低锁冲突了么。</span></p>
  <p data-lake-id="u005aef5f" id="u005aef5f"><span data-lake-id="u1648feac" id="u1648feac" class="lake-fontsize-12">​</span><br></p>
  <p data-lake-id="u980636a4" id="u980636a4"><span data-lake-id="ub508f184" id="ub508f184" class="lake-fontsize-12">比如一个用户，有10个占用库存请求，每次占用1个，那么就可以提供一个批量占用的接口，让上游一次性把10个占用合并一起，这样数据库只需要做一次更新就行了。</span></p>
  <p data-lake-id="u0b154f6c" id="u0b154f6c"><span data-lake-id="u235a856a" id="u235a856a" class="lake-fontsize-12">​</span><br></p>
  <p data-lake-id="ub90b202a" id="ub90b202a"><span data-lake-id="u3d64c957" id="u3d64c957" class="lake-fontsize-12">这个方案有一些局限性，就是不是所有请求都可以合并的，有些场景，如电商的秒杀，用户需要很快的知道反馈，而批量执行就需要有个窗口来聚合，用户是不能接受这种等待窗口的。</span></p>
  <p data-lake-id="u179ff7bf" id="u179ff7bf"><span data-lake-id="u1b9cea3e" id="u1b9cea3e" class="lake-fontsize-12">​</span><br></p>
  <p data-lake-id="u279e1b81" id="u279e1b81"><span data-lake-id="ubb2d4f66" id="ubb2d4f66" class="lake-fontsize-12">所以，在一些异步链路上，是可以用这种方案的。</span></p>
  <p data-lake-id="u257125f4" id="u257125f4"><span data-lake-id="uab5919e6" id="uab5919e6" class="lake-fontsize-12">​</span><br></p>
  <h3 data-lake-id="tlQDj" id="tlQDj"><span data-lake-id="u54ea78df" id="u54ea78df">其他方案</span></h3>
  <p data-lake-id="u2f5fc11c" id="u2f5fc11c"><br></p>
  <p data-lake-id="uca251324" id="uca251324"><span data-lake-id="ue852a28d" id="ue852a28d" class="lake-fontsize-12">当然，还有一些其他的方案，在一些简单场景也可以考虑。</span></p>
  <p data-lake-id="uacd85caf" id="uacd85caf"><span data-lake-id="u4c83179a" id="u4c83179a" class="lake-fontsize-12">​</span><br></p>
  <p data-lake-id="u9097b508" id="u9097b508"><span data-lake-id="uc9a760d3" id="uc9a760d3">1、异步化，通过异步更新的方式，将高并发的更新削峰填谷掉。</span></p>
  <p data-lake-id="u5860cac5" id="u5860cac5"><span data-lake-id="u930585eb" id="u930585eb">​</span><br></p>
  <p data-lake-id="ub2c24e20" id="ub2c24e20"><span data-lake-id="u0e14d1fa" id="u0e14d1fa">2、把update转换成insert，直接插入一次占用记录，然后异步统计剩余库存，或者通过SQL统计流水方式计算剩余库存。</span></p>
  <p data-lake-id="uae8bdbe4" id="uae8bdbe4"><span data-lake-id="u2a119694" id="u2a119694">​</span><br></p>
  <p data-lake-id="u583fa865" id="u583fa865"><span data-lake-id="u1d49aad0" id="u1d49aad0">3、SQL限流，这是一种保护数据库的手段，就是不要让这些请求都打到数据库上。提前拒绝掉。</span></p>
 </body>
</html>